import {
	Scene,
	MeshBasicMaterial,
	Color,
	ShaderMaterial,
	Vector2,
	WebGLRenderTarget,
	LinearFilter,
	RGBAFormat
} from 'three';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
import { ShaderReplacement } from './ShaderReplacement.js';
import { Pass } from 'three/examples/jsm/postprocessing/Pass.js';

const v1 = /* glsl */`
varying vec2 vUv;
uniform sampler2D mainTex;
uniform sampler2D outlineTex;
uniform float thickness;
uniform float opacity;
uniform vec2 resolution;
void main() {
	vec4 pix = texture2D( outlineTex, vUv );
	float t = thickness;
	if(distance(pix.rgb,vec3(1.0,.42,.067))<.1){
		t = 1.0;
	}
	vec2 resMult = 1.0 / resolution;
	vec2 offset1 = vec2( 0.0, 1.0 ) * t * resMult;
	vec2 offset2 = vec2( 0.0, -1.0 ) * t * resMult;
	vec2 offset3 = vec2( 1.0, 0.0 ) * t * resMult;
	vec2 offset4 = vec2( -1.0, 0.0 ) * t * resMult;


	vec4 pix1 = texture2D( outlineTex, vUv + offset1 );
	vec4 pix2 = texture2D( outlineTex, vUv + offset2 );
	vec4 pix3 = texture2D( outlineTex, vUv + offset3 );
	vec4 pix4 = texture2D( outlineTex, vUv + offset4 );

	bool onBorder =
		pix.a != 0.0 &&
		(
			pix.rgb != pix1.rgb ||
			pix.rgb != pix2.rgb ||
			pix.rgb != pix3.rgb ||
			pix.rgb != pix4.rgb
		);

	float weights = pix1.a + pix2.a + pix3.a + pix4.a;
	vec3 color = pix1.rgb * pix1.a + pix2.rgb * pix2.a + pix3.rgb * pix3.a + pix4.rgb * pix4.a;
	color /= weights;
	color = clamp( color, 0.0, 1.0 );

	float alpha = onBorder || weights != 0.0 && pix.a == 0.0 ? opacity : 0.0;

	float enda = smoothstep(0.0,1.0,alpha);

	vec4 main = texture2D( mainTex, vUv );
	gl_FragColor = LinearTosRGB(mix( main, vec4( color, 1.0 ), enda ));

}
`;

export let compositeShader = {

	uniforms: {

		opacity: { value: 1 },
		thickness: { value: 1 },
		resolution: { value: new Vector2() },
		mainTex: { value: null },
		outlineTex: { value: null },

	},
	vertexShader: /* glsl */`
		varying vec2 vUv;
		void main() {

			#include <begin_vertex>
			#include <project_vertex>
			vUv = uv;

		}
	`,
	fragmentShader: /* glsl */`
		varying vec2 vUv;
		uniform sampler2D mainTex;
		uniform sampler2D outlineTex;
		uniform float thickness;
		uniform float opacity;
		uniform vec2 resolution;
		void main() {

			vec2 resMult = 1.0 / resolution;
			vec2 offset1 = vec2( 0.0, 1.0 ) * thickness * resMult;
			vec2 offset2 = vec2( 0.0, -1.0 ) * thickness * resMult;
			vec2 offset3 = vec2( 1.0, 0.0 ) * thickness * resMult;
			vec2 offset4 = vec2( -1.0, 0.0 ) * thickness * resMult;

			vec4 pix = texture2D( outlineTex, vUv );
			vec4 pix1 = texture2D( outlineTex, vUv + offset1 );
			vec4 pix2 = texture2D( outlineTex, vUv + offset2 );
			vec4 pix3 = texture2D( outlineTex, vUv + offset3 );
			vec4 pix4 = texture2D( outlineTex, vUv + offset4 );

			bool onBorder =
				pix.a != 0.0 &&
				(
					pix.rgb != pix1.rgb ||
					pix.rgb != pix2.rgb ||
					pix.rgb != pix3.rgb ||
					pix.rgb != pix4.rgb
				);

			float weights = pix1.a + pix2.a + pix3.a + pix4.a;
			vec3 color = pix1.rgb * pix1.a + pix2.rgb * pix2.a + pix3.rgb * pix3.a + pix4.rgb * pix4.a;
			color /= weights;
			color = clamp( color, 0.0, 1.0 );

			float alpha = onBorder || weights != 0.0 && pix.a == 0.0 ? opacity : 0.0;

			float enda = smoothstep(0.0,1.0,alpha);

			vec4 main = texture2D( mainTex, vUv );
			gl_FragColor =(mix( main, vec4( color, 1.0 ), enda ));
		}
	`
};

class BasicShaderReplacement extends ShaderReplacement {

	constructor() {

		super();
		this.colorMap = new WeakMap();

	}

	createMaterial(object) {

		if (object.isLine2) {

			return new LineMaterial();

		} else {

			return new MeshBasicMaterial();

		}

	}

	updateUniforms(object, material, target) {

		const colorMap = this.colorMap;
		let color = 0;
		let opacity = 0;
		if (colorMap.has(object)) {

			color = colorMap.get(object);
			opacity = 1;

		}

		target.side = material.side;
		target.opacity = opacity;
		if (color instanceof Color) {

			target.color.copy(color);

		} else {

			target.color.set(color);

		}

		if (target instanceof LineMaterial) {

			target.linewidth = material.linewidth;
			target.resolution.copy(material.resolution);

		}

	}

}

export class PixelOutlinePass extends Pass {

	constructor(camera, LinearTosRGB = false) {

		super();

		const auxScene = new Scene();
		auxScene.autoUpdate = false;

		this.renderTarget = new WebGLRenderTarget(1, 1, {
			minFilter: LinearFilter,
			magFilter: LinearFilter,
			format: RGBAFormat,
		});
		this.LinearTosRGB = LinearTosRGB;
		this.setLinearTosRGB(LinearTosRGB);
		this.replacer = new BasicShaderReplacement();
		this.colorMap = new Map();
		this.constantMap = new Map()
		this.objects = [];
		this.auxScene = auxScene;
		this.camera = camera;
		this.needsSwap = true;

		this.scene = null;
		this.renderDepth = false;
		this.thickness = 1;
		this.opacity = 1;

		window.outlinePass = this;
	}

	setConstantOutline(color, objects) {
		this.constantMap.clear();
		for (let i = 0; i < objects.length; i++) {
			this.constantMap.set(objects[i], color);
		}
	}

	setLinearTosRGB(LinearTosRGB) {
		// if(LinearTosRGB){
		compositeShader.fragmentShader = v1;
		// }else{
		// 	compositeShader.fragmentShader = v2;
		// }
		this.quad = new Pass.FullScreenQuad(new ShaderMaterial(compositeShader));
	}

	setOutline(color, newObjects) {

		const colors = this.colorMap;
		const objects = this.objects;
		for (let i = 0, l = newObjects.length; i < l; i++) {

			const o = newObjects[i];
			if (!colors.has(o)) {

				objects.push(o);

			}

			colors.set(o, color);

		}

	}

	removeOutline(removeObjects) {

		const colors = this.colorMap;
		const objects = this.objects;
		for (let i = 0, l = removeObjects.length; i < l; i++) {

			const o = removeObjects[i];
			colors.delete(o);

			const index = objects.indexOf(o);
			objects.splice(index, 1);

		}

	}

	clearOutlines() {

		this.colorMap.clear();
		this.objects.length = 0;

		this.constantMap.forEach((value, key) => {
			this.colorMap.set(key, value);
			this.objects.push(key);
		});

	}

	dispose() {

		this.clearOutlines();
		this.replacer.dispose();
		this.renderTarget.dispose();

	}

	setSize(w, h) {

		this.renderTarget.setSize(w, h);
		this.quad.material.uniforms.resolution.value.set(w, h);

	}

	render(renderer, writeBuffer, readBuffer) {

		const colorMap = this.colorMap;
		const objects = this.objects;
		const replacer = this.replacer;
		const camera = this.camera;
		const renderTarget = this.renderTarget;
		const quad = this.quad;

		let scene = null;
		if (this.renderDepth) {

			scene = this.scene;

		} else {

			scene = this.auxScene;
			scene.children = objects;

		}

		const originalSceneBackground = scene.background;
		const originalClearColor = renderer.getClearColor(new Color());
		const originalClearAlpha = renderer.getClearAlpha();
		scene.background = null;
		renderer.setClearColor(0);
		renderer.setClearAlpha(0);
		replacer.colorMap = colorMap;
		replacer.replace(scene, true, true);

		renderer.setRenderTarget(renderTarget);
		renderer.clear();
		renderer.render(scene, camera);

		renderer.setRenderTarget(this.renderToScreen ? null : writeBuffer);
		quad.material.uniforms.mainTex.value = readBuffer.texture;
		quad.material.uniforms.outlineTex.value = renderTarget.texture;
		quad.material.uniforms.thickness.value = this.thickness;
		quad.material.uniforms.opacity.value = this.opacity;
		quad.render(renderer);

		scene.background = originalSceneBackground;
		renderer.setClearColor(originalClearColor);
		renderer.setClearAlpha(originalClearAlpha);
		replacer.reset(scene, true);

	}

}

